/*  Sgvizler JavaScript SPARQL Google Visualization, version 0.1.0
 *  (c) 2011 Martin G. Skjæveland
 *
 *  Sgvizler is freely distributable under the terms of an MIT-style license.
 *  For details, see the Sgvizler web site: http://sws.ifi.uio.no/sgvizler/
 *
 *  Relies on Google Visualiztion API, jQuery, prototype.
 *--------------------------------------------------------------------------*/

jQuery.noConflict();

var sgvizler = new function () {

    //// URLs:
    // Note same origin policy, see e.g. http://en.wikipedia.org/wiki/Same_origin_policy
    // *** this.home         = "http://sws.ifi.uio.no/project/npd/sgvizler/";
    // *** this.endpoint     = "http://sws.ifi.uio.no/sparql/npd";
    // *** this.endpointURL  = this.endpoint + "?output=text&amp;query=";
    // *** this.validatorURL = "http://sws.ifi.uio.no/sparqler/validator?languageSyntax=SPARQL&amp;outputFormat=sparql&amp;linenumbers=true&amp;query=";
    
    //// Prefixes included in queries:
    this.namespaces = {
	"rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
	"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
	"owl" : "http://www.w3.org/2002/07/owl#",
	"xsd" : "http://www.w3.org/2001/XMLSchema#"
    };

    //// URL parameter names:
    this.paramQuery  = 'query'; // SPARQL query, prefixes defined in this.namespaces not neccessary. 
    this.paramChart  = 'c';     // chart type
    this.paramWidth  = 'w';     // width of the chart
    this.paramHeight = 'h';     // height of the chart

    //// URL parameter default values:
    this.chartWDefault  = '800';
    this.chartHDefault = '400';
    this.chartDefault   = 'line';
    this.queryDefault = 'SELECT ?class count(?instance)\nWHERE{ ?instance a ?class }\nGROUP BY ?class\nORDER BY ?class';

    //// google chart draw() options:
    this.gvizChartArea = {left:"5%", top:"5%", width:"75%", height:"80%"};
    this.gvizGeoRegion = '154'; // region to display
    this.gvizGeoColors = [0xFF8747, 0xFFB581, 0xc06000];
    this.gvizGeoMapDataMode = 'markers';
    this.gvizGeoChartBackgroundColor = 'red'; // TODO

    //// #id's to html elements:
    // (Note also that the functions this.setGChartType and this.setGChartOptions 
    // reference html elements, the options in this.formChart.)
    this.chartCon     = 'gchart';    // #id to the div to hold the chart
    this.queryForm    = 'formQuery'; // 
    this.queryTxt     = 'cQuery';    // query text area.
    this.formQuery    = 'strQuery';  // hidden query string. "trick" taken from snorql.
    this.formWidth    = 'strWidth';  // 
    this.formHeight   = 'strHeight'; // 
    this.formChart    = 'optChart';  // 
    this.prefixCon    = 'cPrefix';   // print prefixes
    this.messageCon   = 'cMessage';  // print messages
    //this.JSONCon    = 'cJSON';     // 
    
    
    ///////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////

    this.go = function(){
	var p = sgvizler.getUrlParams(),
	    u = sgvizler.setUserInput(p);
	sgvizler.displayPrefixes();
	sgvizler.displayUserInput(u);
	if (p[sgvizler.paramQuery]){
	    var chart = sgvizler.getGChartType(u[sgvizler.paramChart]),
	        opt = sgvizler.getGChartOptions(u);
	    google.visualization.events.addListener(chart, 'error', function(err){
		    sgvizler.displayMessage("<p>Error drawing chart: " + err.message + "</p>"); }); 
	    sgvizler.drawGChart(u[sgvizler.paramQuery], function(data){
		    var datatable = new google.visualization.DataTable(data);
		    chart.draw(datatable, opt); });
	}
    }
    
    this.displayUserInput = function(userdata){
	$(sgvizler.queryTxt).update(userdata[sgvizler.paramQuery]);	
	$(sgvizler.formWidth).setValue(userdata[sgvizler.paramWidth]);
	$(sgvizler.formHeight).setValue(userdata[sgvizler.paramHeight]);
	jQuery('#'+sgvizler.formChart).val(userdata[sgvizler.paramChart]);
    }
    
    this.setUserInput = function(param){
	var userdata = {};
	userdata[sgvizler.paramWidth] = param[sgvizler.paramWidth] ? param[sgvizler.paramWidth] : sgvizler.chartWDefault;
	userdata[sgvizler.paramHeight] = param[sgvizler.paramHeight] ? param[sgvizler.paramHeight] : sgvizler.chartHDefault;
	userdata[sgvizler.paramChart] = param[sgvizler.paramChart] ? param[sgvizler.paramChart] : sgvizler.chartDefault;
	userdata[sgvizler.paramQuery] = param[sgvizler.paramQuery] ? param[sgvizler.paramQuery] : sgvizler.queryDefault;
	return userdata;
    }
    
    this.displayMessage = function (message){
	$(sgvizler.messageCon).update(message);
    }

    this.displayPrefixes = function (){
	var prefix = sgvizler.getPrefixes()
	    .replace(/</g,'&lt;')
	    .replace(/>/g,'&gt;');
	$(sgvizler.prefixCon).update(prefix);
    }

    this.resetPage = function() {
        document.location = sgvizler.home;
    }

    this.submitQuery = function() {
	$(sgvizler.formQuery).setValue($(sgvizler.queryTxt).getValue());
        $(sgvizler.queryForm).submit();
    }

    this.getPrefixes = function() {
        prefixes = '';
        for (prefix in sgvizler.namespaces) {
            var uri = sgvizler.namespaces[prefix];
            prefixes += 'PREFIX ' + prefix + ': <' + uri + '>\n';
        }
        return prefixes;
    }    

    this.getUrlParams = function () {
	var urlParams = {},
	    e,
            a = /\+/g,  // regex replacing '+' with a space
            r = /([^&=]+)=?([^&]*)/g,
            d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
            q = window.location.search.substring(1);
	
	while (e = r.exec(q))
	    if(e[2].length > 0)
		urlParams[d(e[1])] = d(e[2]);
	return urlParams;
    }

    this.getGChartType = function(charttype){
	var div = document.getElementById(sgvizler.chartCon);
	if(charttype == 'area')
	    return new google.visualization.AreaChart(div);
	if(charttype == 'pie')
	    return new google.visualization.PieChart(div);
	if(charttype == 'line')
	    return new google.visualization.LineChart(div);
	if(charttype == 'bar')
	    return new google.visualization.BarChart(div);
	if(charttype == 'scatter')
	    return new google.visualization.ScatterChart(div);
	if(charttype == 'candle')
	    return new google.visualization.CandlestickChart(div);
	if(charttype == 'org')
	    return new google.visualization.OrgChart(div);
	if(charttype == 'tree')
	    return new google.visualization.TreeMap(div);
	if(charttype == 'geochart')
	    return new google.visualization.GeoChart(div);
	if(charttype == 'timeline')
	    return new google.visualization.AnnotatedTimeLine(div);
	if(charttype == 'geomap')
	    return new google.visualization.GeoMap(div);
	if(charttype == 'motion')
	    return new google.visualization.MotionChart(div);
	if(charttype == 'map')
	    return new google.visualization.Map(div);
	return new google.visualization.ColumnChart(div);
    }

    this.getGChartOptions = function(u){
	var options = {'width': u[sgvizler.paramWidth], 
		       'height': u[sgvizler.paramHeight], 
		       chartArea: sgvizler.gvizChartArea
		      };

	if(u[sgvizler.paramChart] == 'geomap' || u[sgvizler.paramChart] == 'geochart'){
	    options['dataMode'] = sgvizler.gvizGeoMapDataMode;
	    options['region'] = sgvizler.gvizGeoRegion;
	    options['colors'] = sgvizler.gvizGeoColors;
	    options['backgroundColor'] = sgvizler.gvizGeoChartBackgroundColor;
	}
	else if(u[sgvizler.paramChart] == 'tree'){
	    sgvizler.setChartContainerOptions(u); // TODO, does this work?
	}
	else if(u[sgvizler.paramChart] == 'map'){
	    options['showTip'] = true;
	    options['useMapTypeControl'] = true;
	    sgvizler.setChartContainerOptions(u);
	}
	return options;
    }

    this.setChartContainerOptions = function(u){
	jQuery('#'+sgvizler.chartCon).css('width', u[sgvizler.paramWidth]);
	jQuery('#'+sgvizler.chartCon).css('height', u[sgvizler.paramHeight]);
    }

    this.drawGChart = function(query, callback){
	sgvizler.getQueryResults(query, sgvizler.parseJSONSparql2Google, callback);
    }
    
    this.getQueryResults = function(query, resultparser, callback){
	sgvizler.displayMessage("<p>Sending query...</p>");
	var encodedQuery = encodeURIComponent(sgvizler.getPrefixes() + query);
	jQuery.getJSON(sgvizler.endpoint,
		       {query: sgvizler.getPrefixes() + query, output: "json"})
	    .error(function() { 
		sgvizler.displayMessage(
		    "<p>Error querying endpoint. Possible errors:<ul>" +
		    "<li>Malformed SPARQL query, <a href='" + 
		    sgvizler.validatorURL + encodedQuery +
		    "'>check if it validates</a>.</li>" +
		    "<li>SPARQL endpoint down, check if <a href='" + 
		    sgvizler.endpointURL  + encodedQuery + "'>" +
		    "query runs at endpoint</a>.</li>" +
		    "</ul></p>");
	    })
	    .success(function(data) {
		    jQuery.parseJSON(data);
		    var noRows = sgvizler.countRowsJSONSparql(data)
		    if(noRows == null){   
			sgvizler.displayMessage("Unknown error.");
		    } else if(noRows == 0){ 
			sgvizler.displayMessage("<p>Query returned no results.</p>");
		    } else{
			sgvizler.displayMessage(
			    "<p>Received " + noRows + " rows. Drawing chart...<br/>" + 
				"<a target='_blank' href='" + sgvizler.endpointURL  + encodedQuery + "'>" +
				"View query results</a> (in new window)</p>");
			data = resultparser(data);
			callback(data);
		    }
		})
	//.complete(function() { })
	; // end .getJSON
    }
    

    //// JSON parser: SPARQL JSON to "GOOGLE JSON".
    // variable notation: xtable, xcol(s), xrow(s) -- x is 's'(parql) or 'g'(oogle).

    this.countRowsJSONSparql = function(stable){
	if(typeof (stable["results"])["bindings"] !== 'undefined'){
	    return (stable["results"])["bindings"].length;
	} else return null;
    }
    
    this.parseJSONSparql2Google = function(stable){
	var gcols = [],
	    grows = [],
	    gdatatype = [], // for easy reference of datatypes
     	    scols = (stable["head"])["vars"],
	    srows = (stable["results"])["bindings"];

	for (var c = 0, l = scols.length; c < l; c++) {
	    gdatatype[c] = sgvizler.getGoogleJsonDatatype(
		(((stable["results"])["bindings"])[0])[scols[c]]); // NB! 
	    gcols[c] = {"id": scols[c], "label": scols[c], "type": gdatatype[c]};
	}

	// loop rows
	for (var r = 0, lr = srows.length; r < lr; r++) {
	    var srow = srows[r],
	        grow = [];
	    // loop cells
	    for (var c = 0, lc = scols.length; c < lc; c++) {
		var value = sgvizler.getGoogleJsonValue(
		    (srow[scols[c]])['value'], gdatatype[c]);
		grow[c] = {"v": value};
	    }
	    grows[r] = {"c": grow};
	}
	return {"cols": gcols, "rows": grows};
    }

    this.getGoogleJsonValue = function(value, datatype){
	if(datatype == 'number'){
	    return Number(value);
	} else if(datatype == 'date'){
	    //assume format yyyy-MM-dd
	    return new Date(value.substr(0,4), 
			    value.substr(5,2), 
			    value.substr(8,2));
	} else if(datatype == 'datetime'){
	    //assume format yyyy-MM-ddZHH:mm:ss
	    return new Date(value.substr(0,4), 
			    value.substr(5,2), 
			    value.substr(8,2),
			    value.substr(11,2),
			    value.substr(14,2),
			    value.substr(17,2));
	} else if(datatype == 'timeofday'){
	    //assume format HH:mm:ss
	    return new Array(value.substr(0,2), 
			     value.substr(3,2), 
			     value.substr(6,2));
	} else { // datatype == 'string' || datatype == 'boolean'
	    return value;
	}
    }

    this.getGoogleJsonDatatype = function(scell){
	if(scell['type'] == 'typed-literal'){
	    if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#float' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#double' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#decimal' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#int' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#long' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#integer'){
		return 'number';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#boolean'){
		return 'boolean';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#date'){
		return 'date';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#dateTime'){
		return 'datetime';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#time'){
		return 'timeofday';
	    }
	} else {
	    return 'string';
	}
    }
}